package comprador;

import gestor.Reputacao;
import java.util.Date;
import java.util.Enumeration;
import java.util.Vector;

import jade.core.AID;
import jade.core.Agent;
import jade.core.behaviours.WakerBehaviour;
import jade.domain.FIPAAgentManagement.DFAgentDescription;
import jade.domain.FIPAAgentManagement.Property;
import jade.domain.FIPAException;
import jade.domain.FIPANames;
import jade.lang.acl.ACLMessage;
import jade.proto.ContractNetInitiator;
import java.util.TreeMap;
import javax.swing.JOptionPane;
import util.DF;
import util.Mercado;

public class Agente extends Agent  {
	private FrmParam frmParam = null;
	private int total = 0;
	private FrmGUI frmGUI = null;
	final static public String serviceType = "comprador";
	protected String nome;
	protected String nomeProduto;
	protected float precoMax;
	protected float precoMin;
	protected int prazoMax;
	protected int qualidadeMin;
	private int tentativas = 0;

	final float pesoPorUnidadeQualidade = 0.7f;
	final float pesoPorSegundoPrazo = 0.95f;
	final float ajustePrecoMinNovaIteracao = 1.1f;
	final float ajustePrecoMinCompra = 0.7f;
	final float ajustePrecoMinSemVendedores = 1.3f;
	
	@Override
	protected void setup() {
		nome = getLocalName();

		// Obter os parâmetros da interface gráfica
		frmParam = new FrmParam(this);
		frmParam.setVisible(true);
	}

	@Override
	protected void takeDown() {
		DF.deregister(this);
		if(frmParam != null) {
			frmParam.dispose();
			frmParam = null;
		}
		if(frmGUI != null) {
			frmGUI.dispose();
			frmGUI = null;
		}
	}

	protected void start(String productName, float precoMax, float precoMin, int prazoMax, int qualidadeMin) {
		boolean success;
		frmParam = null;
		this.nomeProduto = productName;
		this.precoMax = precoMax;
		this.precoMin = precoMin;
		this.prazoMax = prazoMax;
		this.qualidadeMin = qualidadeMin;

		// Mostrar a interface gráfica
		frmGUI = new FrmGUI(this);
		frmGUI.setVisible(true);

		// Registar no DF
		success = registerServiceInDF();
		if(!success) return;

		comecarCompra();
	}

	protected void stop() {
		frmParam = null;
		frmGUI = null;
		doDelete();
	}

	private boolean registerServiceInDF() {
		try {
			DF.register(this, nome, serviceType, new Property("nome-produto", nomeProduto));
			return true;
		} catch (FIPAException ex) {
			JOptionPane.showMessageDialog(null, "Erro ao registar este serviço comprador no DF:\n" + ex.getMessage(), nome, JOptionPane.ERROR_MESSAGE);
			doDelete();
			return false;
		}
	}

	private DFAgentDescription[] procurarVendedores() {
		frmGUI.addLog("A procurar vendedores...");
		DFAgentDescription[] args = null;
		try {
			args = DF.search(this, vendedor.Agente.serviceType, new Property("nome-produto", nomeProduto));
		} catch (FIPAException e1) {
			e1.printStackTrace();
		}

		return args;
	}

	private void comecarCompra() {
		// Obter lista de vendedores do produto
		DFAgentDescription[] args = procurarVendedores();

		// Se encontrar vendedores comecar a negociar
		if (args != null && args.length > 0) {
			frmGUI.addLog("Encontrei "+ args.length + " vendedores de " + nomeProduto);
			total = args.length;
			negoceia(args);
		}
		else {
			// Tentar novamente após 1 segundo
			addBehaviour(new WakerBehaviour(this, 1000) {

				@Override
				protected void onWake() {
					comecarCompra();
				}

			});
		}
	}

	private void negoceia(DFAgentDescription[] agentes) {

		frmGUI.addLog("A tentar comprar "+ nomeProduto +" de um de total de "+ total +" vendedores.");

		// Fill the CFP message
		ACLMessage msg = new ACLMessage(ACLMessage.CFP);
		int count = 0;
		for (int i = 0; i < total; ++i) {
			Reputacao rep = Mercado.getRate(this, agentes[i].getName().getLocalName());
			if(rep.getNegative() <= rep.getPositive()) {
				msg.addReceiver(agentes[i].getName());
				count++;
			}
		}

		if(count == 0) {
			frmGUI.addLog("Nenhum vendedor com reputação positiva.");
			// Tentar novamente após 1 segundo
			addBehaviour(new WakerBehaviour(this, 1000) {

				@Override
				protected void onWake() {
					comecarCompra();
				}

			});
			return;
		}

		msg.setProtocol(FIPANames.InteractionProtocol.FIPA_CONTRACT_NET);
		msg.setOntology("initial");
		// We want to receive a reply in 10 secs
		msg.setReplyByDate(new Date(System.currentTimeMillis() + 10000));

		TreeMap<String, String> atributos = new TreeMap<String, String>();
		atributos.put("nome-produto", nomeProduto);
		atributos.put("preco-max", "" + precoMax);
		atributos.put("prazo-max", "" + prazoMax);
		atributos.put("qualidade-min", "" + qualidadeMin);
		msg.setContent(Mercado.propertiesToMessage(atributos));

		/**
		 * Behavior de negociação do comprador
		 */
		addBehaviour(new ContractNetInitiator(this, msg) {

			float precoPrometido;
			int qualidadePrometida;
			int prazoPrometido;
			long startMilis;

			protected void handlePropose(ACLMessage propose, Vector v) {
				frmGUI.addLog(propose.getSender().getLocalName() + " propôs " + propose.getContent());
			}

			protected void handleRefuse(ACLMessage refuse) {
				frmGUI.addLog(refuse.getSender().getLocalName() + " recusou: " + refuse.getContent());
			}

			protected void handleFailure(ACLMessage failure) {
				if (failure.getSender().equals(myAgent.getAMS())) {
					// FAILURE notification from the JADE runtime: the receiver
					// does not exist
					frmGUI.addLog("Responder does not exist");
				}
				else {
					frmGUI.addLog("Failed" + failure.getContent());
				}
				// Immediate failure --> we will not receive a response from this agent
				total--;
			}

			protected void handleAllResponses(Vector responses, Vector acceptances) {
				if (responses.size() < total) {
					// Some responder didn't reply within the specified timeout
					frmGUI.addLog("Timeout expired: missing "+(total - responses.size())+" responses");
				}

				// Atributos da melhor proposta
				float melhorPreco = -1.0f;
				int melhorQualidade = -1;
				int melhorPrazo = -1;
				AID melhorNome = null;
				float melhorPontos = -1.0f;
				ACLMessage melhorMsg = null;
				boolean comprarJa = false;
				float preco;
				int prazo, qualidade;
				int count = 0;

				// Percorrer todas as propostas
				for(ACLMessage msg: (Vector<ACLMessage>)responses) {
					if(msg.getPerformative() == ACLMessage.PROPOSE) {
						TreeMap<String, String> atributos = Mercado.messageToProperties(msg.getContent());
						ACLMessage reply = msg.createReply();

						if(!atributos.containsKey("preco") || !atributos.containsKey("prazo") || !atributos.containsKey("qualidade")) {
							reply.setPerformative(ACLMessage.REJECT_PROPOSAL);
							reply.setContent("Atributos em falta!");
							acceptances.addElement(reply);
						}
						else {
							try {
								preco = Float.parseFloat(atributos.get("preco"));
								prazo = Integer.parseInt(atributos.get("prazo"));
								qualidade = Integer.parseInt(atributos.get("qualidade"));

								// Verificar limites
								if(preco > precoMax || qualidade < qualidadeMin || prazo > prazoMax) {
									reply.setPerformative(ACLMessage.REJECT_PROPOSAL);
									reply.setContent("Atributos fora dos limites pedidos");
									acceptances.addElement(reply);
								}
								else {
									if(melhorPontos < 0.0f) {
										melhorPreco = preco;
										melhorQualidade = qualidade;
										melhorPrazo = prazo;
										melhorNome = msg.getSender();
										melhorMsg = reply;
										melhorPontos = evaluateProposal(preco, qualidade, prazo);
									}
									else {
										float pts = evaluateProposal(preco, qualidade, prazo);
										if(pts < melhorPontos) {
											melhorPreco = preco;
											melhorQualidade = qualidade;
											melhorPrazo = prazo;
											melhorNome = msg.getSender();
											melhorMsg = reply;
											melhorPontos = pts;
										}
									}

									reply.setPerformative(ACLMessage.CFP);
									reply.setOntology("iteration");
									reply.setContent(msg.getContent());
									acceptances.addElement(reply);
									count++;
								}
							}
							catch(NumberFormatException ex) {
								reply.setPerformative(ACLMessage.REJECT_PROPOSAL);
								reply.setContent("Atributos inválidos!");
								acceptances.addElement(reply);
							}
						}
					}
				}

				if(acceptances.isEmpty() || count == 0) { // Nenhum vendedor interessado
					precoMin = Math.min(precoMin *  ajustePrecoMinSemVendedores, precoMax);
					novaCompra();
					return;
				}

				// Analisar a melhor proposta e decidir se compra já ou avança para nova iteração
				
				if(melhorPontos <= precoMin) {
					comprarJa = true;
					precoMin = Math.min(precoMin * ajustePrecoMinCompra, precoMax);

					startMilis = System.currentTimeMillis();
					precoPrometido = melhorPreco;
					prazoPrometido = melhorPrazo;
					qualidadePrometida = melhorQualidade;
				}
				else
					precoMin = Math.min(precoMin * ajustePrecoMinNovaIteracao, precoMax);

				// Atribuir as classificações aos atributos ou aceitar a melhor proposta
				for(ACLMessage msg: (Vector<ACLMessage>)acceptances) {
					if(comprarJa) {
						if(msg == melhorMsg)
							msg.setPerformative(ACLMessage.ACCEPT_PROPOSAL);
						else
							msg.setPerformative(ACLMessage.REJECT_PROPOSAL);
					}
					else {
						// Classificar atributos
						if(msg.getPerformative() == ACLMessage.CFP){
							TreeMap<String, String> atributos = Mercado.messageToProperties(msg.getContent());
							preco = Float.parseFloat(atributos.get("preco"));
							prazo = Integer.parseInt(atributos.get("prazo"));
							qualidade = Integer.parseInt(atributos.get("qualidade"));
							atributos.remove("cpreco");
							atributos.remove("cqualidade");
							atributos.remove("cprazo");


							if(preco <= precoMin * 1.2f)
								atributos.put("cpreco", "bom");
							else if(preco <= precoMin * 1.5f)
								atributos.put("cpreco", "suficiente");
							else
								atributos.put("cpreco", "mau");

							if(prazo <= melhorPrazo)
								atributos.put("cprazo", "bom");
							else if(prazo >= melhorPrazo + 2)
								atributos.put("cprazo", "suficiente");
							else
								atributos.put("cprazo", "mau");

							if(qualidade >= melhorQualidade)
								atributos.put("cqualidade", "bom");
							else if(qualidade >= melhorQualidade - 1)
								atributos.put("cqualidade", "suficiente");
							else
								atributos.put("cqualidade", "mau");

							msg.setContent(Mercado.propertiesToMessage(atributos));
						}
					}
				}

				frmGUI.addLog("Novo preço objectivo: " + precoMin);

				if(!comprarJa) {
					frmGUI.addLog("Nova iteração de compra");
					newIteration(acceptances);
				}
			}

			protected void handleInform(ACLMessage inform) {
				frmGUI.addLog("Produto recebido do vendedor " + inform.getSender().getLocalName() + ": " + inform.getContent());
				long milisPassados = System.currentTimeMillis() - startMilis;
				TreeMap<String, String> atributos = Mercado.messageToProperties(inform.getContent());

				if(!atributos.containsKey("preco") || !atributos.containsKey("qualidade")) {
					frmGUI.addLog("Mensagem de entrega inválida!");
				}
				else {
					float preco = Float.parseFloat(atributos.get("preco"));
					int qualidade = Integer.parseInt(atributos.get("qualidade"));
					int segundosPassados = Math.round(milisPassados / 1000.0f);
					frmGUI.addStat(preco, qualidade);

					if(qualidade < qualidadePrometida || segundosPassados > prazoPrometido) {
						frmGUI.addLog("Contrato não cumprido");
						Mercado.rateNegative(myAgent, inform.getSender().getLocalName());
					}
					else {
						frmGUI.addLog("Contrato cumprido");
						Mercado.ratePositive(myAgent, inform.getSender().getLocalName());
					}
				}

				novaCompra();
			}

			private float evaluateProposal(float preco, int qualidade, int prazo) {
				float pontos = preco;
				if(qualidade > qualidadeMin)
					pontos *= (pesoPorUnidadeQualidade / (qualidade - qualidadeMin));
				if(prazo > prazoMax)
					pontos *= (pesoPorSegundoPrazo / (prazoMax - prazo));

				return pontos;
			}

			private void novaCompra() {
				myAgent.removeBehaviour(this);
				comecarCompra();
			}
		} );
	}
}